/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry.jul;

import com.jakewharton.nopen.annotation.Open;

import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.protocol.Message;
import io.sentry.protocol.SdkVersion;
import io.sentry.util.CollectionUtils;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.logging.ErrorManager;
import java.util.logging.Filter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import org.slf4j.MDC;

/**
 * Logging handler in charge of sending the java.util.logging records to a Sentry server.
 *
 * @since 2021-06-15
 */
@Open
public class SentryHandler extends Handler {
    /**
     * Name of the {@link SentryEvent} extra property containing the Thread id.
     */
    public static final String THREAD_ID = "thread_id";
    /**
     * If true, <code>String.format()</code> is used to render parameterized log messages instead of
     * <code>MessageFormat.format()</code>; Defaults to false.
     */
    private boolean printfStyle;

    private @NotNull Level minimumBreadcrumbLevel = Level.INFO;
    private @NotNull Level minimumEventLevel = Level.SEVERE;

    /**
     * Creates an instance of SentryHandler.
     */
    public SentryHandler() {
        this(new SentryOptions(), true);
    }

    /**
     * Creates an instance of SentryHandler.
     *
     * @param options the SentryOptions
     */
    public SentryHandler(final @NotNull SentryOptions options) {
        this(options, true);
    }

    /**
     * Creates an instance of SentryHandler.
     */
    @TestOnly
    SentryHandler(final @NotNull SentryOptions options, final boolean configureFromLogManager) {
        setFilter(new DropSentryFilter());
        if (configureFromLogManager) {
            retrieveProperties();
        }
        if (!Sentry.isEnabled()) {
            options.setEnableExternalConfiguration(true);
            options.setSdkVersion(createSdkVersion(options));
            Sentry.init(options);
        }
    }

    @Override
    public void publish(final @NotNull LogRecord record) {
        // Do not log the event if the current thread is managed by sentry
        if (!isLoggable(record)) {
            return;
        }
        try {
            if (record.getLevel().intValue() >= minimumEventLevel.intValue()) {
                Sentry.captureEvent(createEvent(record));
            }
            if (record.getLevel().intValue() >= minimumBreadcrumbLevel.intValue()) {
                Sentry.addBreadcrumb(createBreadcrumb(record));
            }
        } catch (RuntimeException e) {
            reportError(
                "An exception occurred while creating a new event in Sentry",
                e,
                ErrorManager.WRITE_FAILURE);
        }
    }

    /**
     * Retrieves the properties of the logger.
     */
    private void retrieveProperties() {
        final LogManager manager = LogManager.getLogManager();
        final String className = SentryHandler.class.getName();
        setPrintfStyle(Boolean.parseBoolean(manager.getProperty(className + ".printfStyle")));
        setLevel(parseLevelOrDefault(manager.getProperty(className + ".level")));
        final String minimumBreadCrumbLevel =
            manager.getProperty(className + ".minimumBreadcrumbLevel");
        if (minimumBreadCrumbLevel != null) {
            setMinimumBreadcrumbLevel(parseLevelOrDefault(minimumBreadCrumbLevel));
        }
        final String eventLevel = manager.getProperty(className + ".minimumEventLevel");
        if (eventLevel != null) {
            setMinimumEventLevel(parseLevelOrDefault(eventLevel));
        }
    }

    /**
     * Transforms a {@link Level} into an {@link SentryLevel}.
     *
     * @param level original level as defined in JUL.
     * @return log level used within sentry.
     */
    private static @Nullable SentryLevel formatLevel(final @NotNull Level level) {
        if (level.intValue() >= Level.SEVERE.intValue()) {
            return SentryLevel.ERROR;
        } else if (level.intValue() >= Level.WARNING.intValue()) {
            return SentryLevel.WARNING;
        } else if (level.intValue() >= Level.INFO.intValue()) {
            return SentryLevel.INFO;
        } else if (level.intValue() >= Level.ALL.intValue()) {
            return SentryLevel.DEBUG;
        } else {
            return null;
        }
    }

    private @NotNull Level parseLevelOrDefault(final @NotNull String levelName) {
        try {
            return Level.parse(levelName.trim());
        } catch (RuntimeException e) {
            return Level.WARNING;
        }
    }

    private @NotNull Breadcrumb createBreadcrumb(final @NotNull LogRecord record) {
        final Breadcrumb breadcrumb = new Breadcrumb();
        breadcrumb.setLevel(formatLevel(record.getLevel()));
        breadcrumb.setCategory(record.getLoggerName());
        if (record.getParameters() != null) {
            try {
                breadcrumb.setMessage(formatMessage(record.getMessage(), record.getParameters()));
            } catch (RuntimeException e) {
                breadcrumb.setMessage(record.getMessage());
            }
        } else {
            breadcrumb.setMessage(record.getMessage());
        }
        return breadcrumb;
    }

    /**
     * Creates {@link SentryEvent} from JUL's {@link LogRecord}.
     *
     * @param record the log record
     * @return the sentry event
     */
    // for the Ohos compatibility we must use old Java Date class
    @SuppressWarnings({"JdkObsolete", "JavaUtilDate"})
    @NotNull
    SentryEvent createEvent(final @NotNull LogRecord record) {
        final SentryEvent event = new SentryEvent(new Date(record.getMillis()));
        event.setLevel(formatLevel(record.getLevel()));
        event.setLogger(record.getLoggerName());

        final Message sentryMessage = new Message();
        sentryMessage.setParams(toParams(record.getParameters()));

        String message = record.getMessage();
        if (record.getResourceBundle() != null
            && record.getResourceBundle().containsKey(record.getMessage())) {
            message = record.getResourceBundle().getString(record.getMessage());
        }
        sentryMessage.setMessage(message);
        if (record.getParameters() != null) {
            try {
                sentryMessage.setFormatted(formatMessage(message, record.getParameters()));
            } catch (RuntimeException e) {
                // local formatting failed, send message and parameters without formatted string
            }
        }
        event.setMessage(sentryMessage);

        final Throwable throwable = record.getThrown();
        if (throwable != null) {
            event.setThrowable(throwable);
        }
        Map<String, String> mdcProperties = MDC.getMDCAdapter().getCopyOfContextMap();
        if (mdcProperties != null) {
            mdcProperties =
                CollectionUtils.filterMapEntries(mdcProperties, entry -> entry.getValue() != null);
            if (!mdcProperties.isEmpty()) {
                event.getContexts().put("MDC", mdcProperties);
            }
        }
        event.setExtra(THREAD_ID, record.getThreadID());
        return event;
    }

    private @NotNull List<String> toParams(final @Nullable Object[] arguments) {
        final List<String> result = new ArrayList<>();
        if (arguments != null) {
            for (Object argument : arguments) {
                if (argument != null) {
                    result.add(argument.toString());
                }
            }
        }
        return result;
    }

    /**
     * Returns formatted Event message when provided the message template and parameters.
     *
     * @param message Message template body.
     * @param parameters Array of parameters for the message.
     * @return Formatted message.
     */
    private @NotNull String formatMessage(
        final @NotNull String message, final @Nullable Object[] parameters) {
        String formatted;
        if (printfStyle) {
            formatted = String.format(message, parameters);
        } else {
            formatted = MessageFormat.format(message, parameters);
        }
        return formatted;
    }

    @Override
    public void flush() {
    }

    @Override
    public void close() throws SecurityException {
        try {
            Sentry.close();
        } catch (RuntimeException e) {
            reportError(
                "An exception occurred while closing the Sentry connection",
                e,
                ErrorManager.CLOSE_FAILURE);
        }
    }

    private @NotNull SdkVersion createSdkVersion(final @NotNull SentryOptions sentryOptions) {
        SdkVersion sdkVersion = sentryOptions.getSdkVersion();

        final String name = BuildConfig.SENTRY_JUL_SDK_NAME;
        final String version = BuildConfig.VERSION_NAME;

        sdkVersion = SdkVersion.updateSdkVersion(sdkVersion, name, version);
        sdkVersion.addPackage("maven:io.sentry:sentry-jul", version);

        return sdkVersion;
    }

    public void setPrintfStyle(final boolean printfStyle) {
        this.printfStyle = printfStyle;
    }

    public void setMinimumBreadcrumbLevel(final @Nullable Level minimumBreadcrumbLevel) {
        if (minimumBreadcrumbLevel != null) {
            this.minimumBreadcrumbLevel = minimumBreadcrumbLevel;
        }
    }

    public @NotNull Level getMinimumBreadcrumbLevel() {
        return minimumBreadcrumbLevel;
    }

    public void setMinimumEventLevel(final @Nullable Level minimumEventLevel) {
        if (minimumEventLevel != null) {
            this.minimumEventLevel = minimumEventLevel;
        }
    }

    public @NotNull Level getMinimumEventLevel() {
        return minimumEventLevel;
    }

    public boolean isPrintfStyle() {
        return printfStyle;
    }

    private static final class DropSentryFilter implements Filter {
        @Override
        public boolean isLoggable(final @NotNull LogRecord record) {
            final String loggerName = record.getLoggerName();
            return loggerName == null
                || !loggerName.startsWith("io.sentry")
                || loggerName.startsWith("io.sentry.samples");
        }
    }
}
